package fr.acinq.eclair.blockchain.bitcoind


import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler}
import fr.acinq.bitcoin.scalacompat.{Script, ScriptElt}
import fr.acinq.eclair.blockchain.OnChainAddressGenerator

import java.util.concurrent.atomic.AtomicReference
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.FiniteDuration
import scala.util.{Failure, Success}

/**
 * Handles the renewal of on-chain addresses generated by bitcoin core and used to receive on-chain funds when channels get closed.
 */
object OnChainAddressRefresher {

  // @formatter:off
  sealed trait Command
  case object RenewPubkeyScript extends Command
  private case class SetPubkeyScript(script: Seq[ScriptElt]) extends Command
  private case class Error(reason: Throwable) extends Command
  private case object Done extends Command
  // @formatter:on

  def apply(generator: OnChainAddressGenerator, finalPubkeyScript: AtomicReference[Seq[ScriptElt]], delay: FiniteDuration): Behavior[Command] = {
    Behaviors.setup { context =>
      Behaviors.withTimers { timers =>
        val refresher = new OnChainAddressRefresher(generator, finalPubkeyScript, context, timers, delay)
        refresher.idle()
      }
    }
  }
}

private class OnChainAddressRefresher(generator: OnChainAddressGenerator,
                                      finalPubkeyScript: AtomicReference[Seq[ScriptElt]],
                                      context: ActorContext[OnChainAddressRefresher.Command],
                                      timers: TimerScheduler[OnChainAddressRefresher.Command], delay: FiniteDuration) {

  import OnChainAddressRefresher._

  /** In that state, we're ready to renew our on-chain address whenever requested. */
  def idle(): Behavior[Command] = Behaviors.receiveMessage {
    case RenewPubkeyScript =>
      context.log.debug("renewing script (current={})", Script.write(finalPubkeyScript.get()).toHex)
      context.pipeToSelf(generator.getReceivePublicKeyScript()) {
        case Success(script) => SetPubkeyScript(script)
        case Failure(reason) => Error(reason)
      }
      renewing()
    case cmd =>
      context.log.debug("ignoring command={} while idle", cmd)
      Behaviors.same
  }

  /** We ignore concurrent requests while waiting for bitcoind to respond. */
  private def renewing(): Behavior[Command] = Behaviors.receiveMessage {
    case SetPubkeyScript(script) =>
      timers.startSingleTimer(Done, delay)
      delaying(script)
    case Error(reason) =>
      context.log.error("cannot renew script", reason)
      idle()
    case cmd =>
      context.log.debug("ignoring command={} while waiting for bitcoin core's response", cmd)
      Behaviors.same
  }

  /**
   * After receiving our new address from bitcoind, we wait before updating our current value.
   * While waiting, we ignore additional requests to renew.
   *
   * This ensures that a burst of requests during a mass force-close use the same final on-chain address instead of
   * creating a lot of address churn on our bitcoin wallet.
   */
  private def delaying(nextScript: Seq[ScriptElt]): Behavior[Command] = Behaviors.receiveMessage {
    case Done =>
      context.log.info("setting script to {}", Script.write(nextScript).toHex)
      finalPubkeyScript.set(nextScript)
      idle()
    case cmd =>
      context.log.debug("rate-limiting command={}", cmd)
      Behaviors.same
  }

}
